/**
 * Copyright 2010-present Facebook.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.widget;

import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ListView;
import com.facebook.*;
import com.facebook.android.R;
import com.facebook.internal.AnalyticsEvents;
import com.facebook.internal.Logger;
import com.facebook.internal.Utility;
import com.facebook.model.GraphPlace;

import java.util.*;

public class PlacePickerFragment extends PickerFragment<GraphPlace>
{
  /**
   * The key for an int parameter in the fragment's Intent bundle to indicate the radius in meters around
   * the center point to search. The default is 1000 meters.
   */
  public static final String RADIUS_IN_METERS_BUNDLE_KEY = "com.facebook.widget.PlacePickerFragment.RadiusInMeters";
  /**
   * The key for an int parameter in the fragment's Intent bundle to indicate what how many results to
   * return at a time. The default is 100 results.
   */
  public static final String RESULTS_LIMIT_BUNDLE_KEY = "com.facebook.widget.PlacePickerFragment.ResultsLimit";
  /**
   * The key for a String parameter in the fragment's Intent bundle to indicate what search text should
   * be sent to the service. The default is to have no search text.
   */
  public static final String SEARCH_TEXT_BUNDLE_KEY = "com.facebook.widget.PlacePickerFragment.SearchText";
  /**
   * The key for a Location parameter in the fragment's Intent bundle to indicate what geographical
   * location should be the center of the search.
   */
  public static final String LOCATION_BUNDLE_KEY = "com.facebook.widget.PlacePickerFragment.Location";
  /**
   * The key for a boolean parameter in the fragment's Intent bundle to indicate that the fragment
   * should display a search box and automatically update the search text as it changes.
   */
  public static final String SHOW_SEARCH_BOX_BUNDLE_KEY = "com.facebook.widget.PlacePickerFragment.ShowSearchBox";

  /**
   * The default radius around the center point to search.
   */
  public static final int DEFAULT_RADIUS_IN_METERS = 1000;
  /**
   * The default number of results to retrieve.
   */
  public static final int DEFAULT_RESULTS_LIMIT = 100;

  private static final int searchTextTimerDelayInMilliseconds = 2 * 1000;

  private static final String ID = "id";
  private static final String NAME = "name";
  private static final String LOCATION = "location";
  private static final String CATEGORY = "category";
  private static final String WERE_HERE_COUNT = "were_here_count";
  private static final String TAG = "PlacePickerFragment";

  private Location location;
  private int radiusInMeters = DEFAULT_RADIUS_IN_METERS;
  private int resultsLimit = DEFAULT_RESULTS_LIMIT;
  private String searchText;
  private Timer searchTextTimer;
  private boolean hasSearchTextChangedSinceLastQuery;
  private boolean showSearchBox = true;
  private EditText searchBox;

  /**
   * Default constructor. Creates a Fragment with all default properties.
   */
  public PlacePickerFragment()
  {
    this(null);
  }

  /**
   * Constructor.
   *
   * @param args a Bundle that optionally contains one or more values containing additional
   *             configuration information for the Fragment.
   */
  public PlacePickerFragment(Bundle args)
  {
    super(GraphPlace.class, R.layout.com_facebook_placepickerfragment, args);
    setPlacePickerSettingsFromBundle(args);
  }

  /**
   * Gets the location to search around. Either the location or the search text (or both) must be specified.
   *
   * @return the Location to search around
   */
  public Location getLocation()
  {
    return location;
  }

  /**
   * Sets the location to search around. Either the location or the search text (or both) must be specified.
   *
   * @param location the Location to search around
   */
  public void setLocation(Location location)
  {
    this.location = location;
  }

  /**
   * Gets the radius in meters around the location to search.
   *
   * @return the radius in meters
   */
  public int getRadiusInMeters()
  {
    return radiusInMeters;
  }

  /**
   * Sets the radius in meters around the location to search.
   *
   * @param radiusInMeters the radius in meters
   */
  public void setRadiusInMeters(int radiusInMeters)
  {
    this.radiusInMeters = radiusInMeters;
  }

  /**
   * Gets the number of results to retrieve.
   *
   * @return the number of results to retrieve
   */
  public int getResultsLimit()
  {
    return resultsLimit;
  }

  /**
   * Sets the number of results to retrieve.
   *
   * @param resultsLimit the number of results to retrieve
   */
  public void setResultsLimit(int resultsLimit)
  {
    this.resultsLimit = resultsLimit;
  }

  /**
   * Gets the search text (e.g., category, name) to search for. Either the location or the search
   * text (or both) must be specified.
   *
   * @return the search text
   */
  public String getSearchText()
  {
    return searchText;
  }

  /**
   * Sets the search text (e.g., category, name) to search for. Either the location or the search
   * text (or both) must be specified. If a search box is displayed, this will update its contents
   * to the specified text.
   *
   * @param searchText the search text
   */
  public void setSearchText(String searchText)
  {
    if (TextUtils.isEmpty(searchText))
    {
      searchText = null;
    }
    this.searchText = searchText;
    if (this.searchBox != null)
    {
      this.searchBox.setText(searchText);
    }
  }

  /**
   * Sets the search text and reloads the data in the control. This is used to provide search-box
   * functionality where the user may be typing or editing text rapidly. It uses a timer to avoid repeated
   * requerying, preferring to wait until the user pauses typing to refresh the data. Note that this
   * method will NOT update the text in the search box, if any, as it is intended to be called as a result
   * of changes to the search box (and is public to enable applications to provide their own search box
   * UI instead of the default one).
   *
   * @param searchText                 the search text
   * @param forceReloadEventIfSameText if true, will reload even if the search text has not changed; if false,
   *                                   identical search text will not force a reload
   */
  public void onSearchBoxTextChanged(String searchText, boolean forceReloadEventIfSameText)
  {
    if (!forceReloadEventIfSameText && Utility.stringsEqualOrEmpty(this.searchText, searchText))
    {
      return;
    }

    if (TextUtils.isEmpty(searchText))
    {
      searchText = null;
    }
    this.searchText = searchText;

    // If search text is being set in response to user input, it is wasteful to send a new request
    // with every keystroke. Send a request the first time the search text is set, then set up a 2-second timer
    // and send whatever changes the user has made since then. (If nothing has changed
    // in 2 seconds, we reset so the next change will cause an immediate re-query.)
    hasSearchTextChangedSinceLastQuery = true;
    if (searchTextTimer == null)
    {
      searchTextTimer = createSearchTextTimer();
    }
  }

  /**
   * Gets the currently-selected place.
   *
   * @return the currently-selected place, or null if there is none
   */
  public GraphPlace getSelection()
  {
    Collection<GraphPlace> selection = getSelectedGraphObjects();
    return (selection != null && !selection.isEmpty()) ? selection.iterator().next() : null;
  }

  public void setSettingsFromBundle(Bundle inState)
  {
    super.setSettingsFromBundle(inState);
    setPlacePickerSettingsFromBundle(inState);
  }

  @Override
  public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState)
  {
    super.onInflate(activity, attrs, savedInstanceState);
    TypedArray a = activity.obtainStyledAttributes(attrs, R.styleable.com_facebook_place_picker_fragment);

    setRadiusInMeters(a.getInt(R.styleable.com_facebook_place_picker_fragment_radius_in_meters, radiusInMeters));
    setResultsLimit(a.getInt(R.styleable.com_facebook_place_picker_fragment_results_limit, resultsLimit));
    if (a.hasValue(R.styleable.com_facebook_place_picker_fragment_results_limit))
    {
      setSearchText(a.getString(R.styleable.com_facebook_place_picker_fragment_search_text));
    }
    showSearchBox = a.getBoolean(R.styleable.com_facebook_place_picker_fragment_show_search_box, showSearchBox);

    a.recycle();
  }

  @Override
  void setupViews(ViewGroup view)
  {
    if (showSearchBox)
    {
      ListView listView = (ListView) view.findViewById(R.id.com_facebook_picker_list_view);

      View searchHeaderView = getActivity().getLayoutInflater().inflate(
          R.layout.com_facebook_picker_search_box, listView, false);

      listView.addHeaderView(searchHeaderView, null, false);

      searchBox = (EditText) view.findViewById(R.id.com_facebook_picker_search_text);

      searchBox.addTextChangedListener(new SearchTextWatcher());
      if (!TextUtils.isEmpty(searchText))
      {
        searchBox.setText(searchText);
      }
    }
  }

  @Override
  public void onAttach(Activity activity)
  {
    super.onAttach(activity);

    if (searchBox != null)
    {
      InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
      imm.showSoftInput(searchBox, InputMethodManager.SHOW_IMPLICIT);
    }
  }

  @Override
  public void onDetach()
  {
    super.onDetach();

    if (searchBox != null)
    {
      InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
      imm.hideSoftInputFromWindow(searchBox.getWindowToken(), 0);
    }
  }

  void saveSettingsToBundle(Bundle outState)
  {
    super.saveSettingsToBundle(outState);

    outState.putInt(RADIUS_IN_METERS_BUNDLE_KEY, radiusInMeters);
    outState.putInt(RESULTS_LIMIT_BUNDLE_KEY, resultsLimit);
    outState.putString(SEARCH_TEXT_BUNDLE_KEY, searchText);
    outState.putParcelable(LOCATION_BUNDLE_KEY, location);
    outState.putBoolean(SHOW_SEARCH_BOX_BUNDLE_KEY, showSearchBox);
  }

  @Override
  void onLoadingData()
  {
    hasSearchTextChangedSinceLastQuery = false;
  }

  @Override
  Request getRequestForLoadData(Session session)
  {
    return createRequest(location, radiusInMeters, resultsLimit, searchText, extraFields, session);
  }

  @Override
  String getDefaultTitleText()
  {
    return getString(R.string.com_facebook_nearby);
  }

  @Override
  void logAppEvents(boolean doneButtonClicked)
  {
    AppEventsLogger logger = AppEventsLogger.newLogger(this.getActivity(), getSession());
    Bundle parameters = new Bundle();

    // If Done was clicked, we know this completed successfully. If not, we don't know (caller might have
    // dismissed us in response to selection changing, or user might have hit back button). Either way
    // we'll log the number of selections.
    String outcome = doneButtonClicked ? AnalyticsEvents.PARAMETER_DIALOG_OUTCOME_VALUE_COMPLETED :
        AnalyticsEvents.PARAMETER_DIALOG_OUTCOME_VALUE_UNKNOWN;
    parameters.putString(AnalyticsEvents.PARAMETER_DIALOG_OUTCOME, outcome);
    parameters.putInt("num_places_picked", (getSelection() != null) ? 1 : 0);

    logger.logSdkEvent(AnalyticsEvents.EVENT_PLACE_PICKER_USAGE, null, parameters);
  }

  @Override
  PickerFragmentAdapter<GraphPlace> createAdapter()
  {
    PickerFragmentAdapter<GraphPlace> adapter = new PickerFragmentAdapter<GraphPlace>(
        this.getActivity())
    {
      @Override
      protected CharSequence getSubTitleOfGraphObject(GraphPlace graphObject)
      {
        String category = graphObject.getCategory();
        Integer wereHereCount = (Integer) graphObject.getProperty(WERE_HERE_COUNT);

        String result = null;
        if (category != null && wereHereCount != null)
        {
          result = getString(R.string.com_facebook_placepicker_subtitle_format, category, wereHereCount);
        }
        else
          if (category == null && wereHereCount != null)
          {
            result = getString(R.string.com_facebook_placepicker_subtitle_were_here_only_format, wereHereCount);
          }
          else
            if (category != null && wereHereCount == null)
            {
              result = getString(R.string.com_facebook_placepicker_subtitle_catetory_only_format, category);
            }
        return result;
      }

      @Override
      protected int getGraphObjectRowLayoutId(GraphPlace graphObject)
      {
        return R.layout.com_facebook_placepickerfragment_list_row;
      }

      @Override
      protected int getDefaultPicture()
      {
        return R.drawable.com_facebook_place_default_icon;
      }

    };
    adapter.setShowCheckbox(false);
    adapter.setShowPicture(getShowPictures());
    return adapter;
  }

  @Override
  LoadingStrategy createLoadingStrategy()
  {
    return new AsNeededLoadingStrategy();
  }

  @Override
  SelectionStrategy createSelectionStrategy()
  {
    return new SingleSelectionStrategy();
  }

  private Request createRequest(Location location, int radiusInMeters, int resultsLimit, String searchText,
                                Set<String> extraFields,
                                Session session)
  {
    Request request = Request.newPlacesSearchRequest(session, location, radiusInMeters, resultsLimit, searchText,
        null);

    Set<String> fields = new HashSet<String>(extraFields);
    String[] requiredFields = new String[]{
        ID,
        NAME,
        LOCATION,
        CATEGORY,
        WERE_HERE_COUNT
    };
    fields.addAll(Arrays.asList(requiredFields));

    String pictureField = adapter.getPictureFieldSpecifier();
    if (pictureField != null)
    {
      fields.add(pictureField);
    }

    Bundle parameters = request.getParameters();
    parameters.putString("fields", TextUtils.join(",", fields));
    request.setParameters(parameters);

    return request;
  }

  private void setPlacePickerSettingsFromBundle(Bundle inState)
  {
    // We do this in a separate non-overridable method so it is safe to call from the constructor.
    if (inState != null)
    {
      setRadiusInMeters(inState.getInt(RADIUS_IN_METERS_BUNDLE_KEY, radiusInMeters));
      setResultsLimit(inState.getInt(RESULTS_LIMIT_BUNDLE_KEY, resultsLimit));
      if (inState.containsKey(SEARCH_TEXT_BUNDLE_KEY))
      {
        setSearchText(inState.getString(SEARCH_TEXT_BUNDLE_KEY));
      }
      if (inState.containsKey(LOCATION_BUNDLE_KEY))
      {
        Location location = inState.getParcelable(LOCATION_BUNDLE_KEY);
        setLocation(location);
      }
      showSearchBox = inState.getBoolean(SHOW_SEARCH_BOX_BUNDLE_KEY, showSearchBox);
    }
  }

  private Timer createSearchTextTimer()
  {
    Timer timer = new Timer();
    timer.schedule(new TimerTask()
    {
      @Override
      public void run()
      {
        onSearchTextTimerTriggered();
      }
    }, 0, searchTextTimerDelayInMilliseconds);

    return timer;
  }

  private void onSearchTextTimerTriggered()
  {
    if (hasSearchTextChangedSinceLastQuery)
    {
      Handler handler = new Handler(Looper.getMainLooper());
      handler.post(new Runnable()
      {
        @Override
        public void run()
        {
          FacebookException error = null;
          try
          {
            loadData(true);
          }
          catch (FacebookException fe)
          {
            error = fe;
          }
          catch (Exception e)
          {
            error = new FacebookException(e);
          }
          finally
          {
            if (error != null)
            {
              OnErrorListener onErrorListener = getOnErrorListener();
              if (onErrorListener != null)
              {
                onErrorListener.onError(PlacePickerFragment.this, error);
              }
              else
              {
                Logger.log(LoggingBehavior.REQUESTS, TAG, "Error loading data : %s", error);
              }
            }
          }
        }
      });
    }
    else
    {
      // Nothing has changed in 2 seconds. Invalidate and forget about this timer.
      // Next time the user types, we will fire a query immediately again.
      searchTextTimer.cancel();
      searchTextTimer = null;
    }
  }

  private class AsNeededLoadingStrategy extends LoadingStrategy
  {
    @Override
    public void attach(GraphObjectAdapter<GraphPlace> adapter)
    {
      super.attach(adapter);

      this.adapter.setDataNeededListener(new GraphObjectAdapter.DataNeededListener()
      {
        @Override
        public void onDataNeeded()
        {
          // Do nothing if we are currently loading data . We will get notified again when that load finishes if the adapter still
          // needs more data. Otherwise, follow the next link.
          if (!loader.isLoading())
          {
            loader.followNextLink();
          }
        }
      });
    }

    @Override
    protected void onLoadFinished(GraphObjectPagingLoader<GraphPlace> loader,
                                  SimpleGraphObjectCursor<GraphPlace> data)
    {
      super.onLoadFinished(loader, data);

      // We could be called in this state if we are clearing data or if we are being re-attached
      // in the middle of a query.
      if (data == null || loader.isLoading())
      {
        return;
      }

      hideActivityCircle();

      if (data.isFromCache())
      {
        // Only the first page can be cached, since all subsequent pages will be round-tripped. Force
        // a refresh of the first page before we allow paging to begin. If the first page produced
        // no data, launch the refresh immediately, otherwise schedule it for later.
        loader.refreshOriginalRequest(data.areMoreObjectsAvailable() ? CACHED_RESULT_REFRESH_DELAY : 0);
      }
    }
  }

  private class SearchTextWatcher implements TextWatcher
  {

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after)
    {
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count)
    {
      onSearchBoxTextChanged(s.toString(), false);
    }

    @Override
    public void afterTextChanged(Editable s)
    {
    }
  }
}
